/* Copyright 2009
 *
 * This program and the accompanying materials
 * are made available under the terms of the
 * Eclipse Public License v1.0 which accompanies
 * this distribution, and is available at
 *
 * 		http://www.eclipse.org/legal/epl-v10.html
 *
 * Unless required by applicable law or agreed to in
 * writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
 * OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing
 * permissions and limitations under the License.
 *
 * Contributors:
 * 	   IBM Corporation - initial API and implementation for JDT/DLTK
 *     Sean W. Quinn - initial adoption for use with PHP from various sources.
 */
package org.eclipse.php.internal.core;

import java.util.HashMap;
import java.util.Map;

import org.eclipse.dltk.internal.core.ElementCache;
import org.eclipse.dltk.internal.core.util.LRUCache;
import org.eclipse.dltk.internal.core.util.Util;
import org.eclipse.php.core.IPHPElement;

/**
 * The cache of java elements to their respective info.
 */
@SuppressWarnings("restriction")
public class PHPModelCache {
	public static boolean VERBOSE = false;

	public static final int DEFAULT_PROJECT_SIZE = 5; // average 25552 bytes per project.
	public static final int DEFAULT_ROOT_SIZE = 50; // average 2590 bytes per root -> maximum size : 25900*BASE_VALUE bytes
	public static final int DEFAULT_PKG_SIZE = 500; // average 1782 bytes per pkg -> maximum size : 178200*BASE_VALUE bytes
	public static final int DEFAULT_OPENABLE_SIZE = 250; // average 6629 bytes per openable (includes children) -> maximum size : 662900*BASE_VALUE bytes
	public static final int DEFAULT_CHILDREN_SIZE = 250 * 20; // average 20 children per openable
	public static final String RATIO_PROPERTY = "org.eclipse.php.core.phpmodelcache.ratio"; //$NON-NLS-1$

	public static final Object NON_EXISTING_JAR_TYPE_INFO = new Object();

	/*
	 * The memory ratio that should be applied to the above constants.
	 */
	protected double memoryRatio = -1;

	/**
	 * Active Java Model Info
	 */
	protected Object modelInfo;

	/**
	 * Cache of open projects.
	 */
	protected HashMap projectCache;

	/**
	 * Cache of open package fragment roots.
	 */
	protected ElementCache rootCache;

	/**
	 * Cache of open package fragments
	 */
	protected ElementCache pkgCache;

	/**
	 * Cache of open compilation unit and class files
	 */
	protected ElementCache openableCache;

	/**
	 * Cache of open children of openable Java Model Java elements
	 */
	protected Map childrenCache;

	/*
	 * Cache of open binary type (inside a jar) that have a non-open parent
	 */
	protected LRUCache jarTypeCache;

	public PHPModelCache() {
		// set the size of the caches in function of the maximum amount of memory available
		double ratio = getMemoryRatio();
		// adjust the size of the openable cache in function of the RATIO_PROPERTY property
		double openableRatio = getOpenableRatio();
		this.projectCache = new HashMap(DEFAULT_PROJECT_SIZE); // NB: Don't use a LRUCache for projects as they are constantly reopened (e.g. during delta processing)
		if (VERBOSE) {
			this.rootCache = new VerboseElementCache((int) (DEFAULT_ROOT_SIZE * ratio), "Root cache"); //$NON-NLS-1$
			this.pkgCache = new VerboseElementCache((int) (DEFAULT_PKG_SIZE * ratio), "Package cache"); //$NON-NLS-1$
			this.openableCache = new VerboseElementCache((int) (DEFAULT_OPENABLE_SIZE * ratio * openableRatio),
					"Openable cache"); //$NON-NLS-1$
		}
		else {
			this.rootCache = new ElementCache((int) (DEFAULT_ROOT_SIZE * ratio));
			this.pkgCache = new ElementCache((int) (DEFAULT_PKG_SIZE * ratio));
			this.openableCache = new ElementCache((int) (DEFAULT_OPENABLE_SIZE * ratio * openableRatio));
		}
		this.childrenCache = new HashMap((int) (DEFAULT_CHILDREN_SIZE * ratio * openableRatio));
		resetJarTypeCache();
	}

	private double getOpenableRatio() {
		String property = System.getProperty(RATIO_PROPERTY);
		if (property != null) {
			try {
				return Double.parseDouble(property);
			}
			catch (NumberFormatException e) {
				// ignore
				Util.log(e, "Could not parse value for " + RATIO_PROPERTY + ": " + property); //$NON-NLS-1$ //$NON-NLS-2$
			}
		}
		return 1.0;
	}

	/**
	 * Returns the info for the element.
	 */
	public Object getInfo(IPHPElement element) {
		switch (element.getElementType()) {
		case IPHPElement.PHP_MODEL:
			return this.modelInfo;
		case IPHPElement.PHP_PROJECT:
			return this.projectCache.get(element);
		case IPHPElement.NAMESPACE_FRAGMENT_ROOT:
			return this.rootCache.get(element);
		case IPHPElement.NAMESPACE_FRAGMENT:
			return this.pkgCache.get(element);
		case IPHPElement.PROGRAM_UNIT:
		case IPHPElement.SCRIPT_FILE:
			return this.openableCache.get(element);
		case IPHPElement.TYPE:
			Object result = this.jarTypeCache.get(element);
			if (result != null)
				return result;
			else
				return this.childrenCache.get(element);
		default:
			return this.childrenCache.get(element);
		}
	}

	/*
	 * Returns the existing element that is equal to the given element if
	 * present in the cache. Returns the given element otherwise.
	 */
	public IPHPElement getExistingElement(IPHPElement element) {
		switch (element.getElementType()) {
		case IPHPElement.PHP_MODEL:
			return element;
		case IPHPElement.PHP_PROJECT:
			return element; // projectCache is a Hashtable and Hashtables don't support get(...)
		case IPHPElement.NAMESPACE_FRAGMENT_ROOT:
			return (IPHPElement) this.rootCache.get(element);
		case IPHPElement.NAMESPACE_FRAGMENT:
			return (IPHPElement) this.pkgCache.get(element);
		case IPHPElement.PROGRAM_UNIT:
		case IPHPElement.SCRIPT_FILE:
			return (IPHPElement) this.openableCache.get(element);
		case IPHPElement.TYPE:
			return element; // jarTypeCache or childrenCache are Hashtables and Hashtables don't support get(...)
		default:
			return element; // childrenCache is a Hashtable and Hashtables don't support get(...)
		}
	}

	protected double getMemoryRatio() {
		if (this.memoryRatio == -1) {
			long maxMemory = Runtime.getRuntime().maxMemory();
			// if max memory is infinite, set the ratio to 4d which corresponds to the 256MB that Eclipse defaults to
			// (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=111299)
			this.memoryRatio = maxMemory == Long.MAX_VALUE ? 4d : ((double) maxMemory) / (64 * 0x100000); // 64MB is the base memory for most JVM
		}
		return this.memoryRatio;
	}

	/**
	 * Returns the info for this element without disturbing the cache ordering.
	 */
	protected Object peekAtInfo(IPHPElement element) {
		switch (element.getElementType()) {
		case IPHPElement.PHP_MODEL:
			return this.modelInfo;
		case IPHPElement.PHP_PROJECT:
			return this.projectCache.get(element);
		case IPHPElement.NAMESPACE_FRAGMENT_ROOT:
			return this.rootCache.peek(element);
		case IPHPElement.NAMESPACE_FRAGMENT:
			return this.pkgCache.peek(element);
		case IPHPElement.PROGRAM_UNIT:
		case IPHPElement.SCRIPT_FILE:
			return this.openableCache.peek(element);
		case IPHPElement.TYPE:
			Object result = this.jarTypeCache.peek(element);
			if (result != null)
				return result;
			else
				return this.childrenCache.get(element);
		default:
			return this.childrenCache.get(element);
		}
	}

	/**
	 * Remember the info for the element.
	 */
	protected void putInfo(IPHPElement element, int info) {
		switch (element.getElementType()) {
		case IPHPElement.PHP_MODEL:
			this.modelInfo = info;
			break;
		case IPHPElement.PHP_PROJECT:
			this.projectCache.put(element, info);
			this.rootCache.ensureSpaceLimit(info, element);
			break;
		case IPHPElement.NAMESPACE_FRAGMENT_ROOT:
			this.rootCache.put(element, info);
			this.pkgCache.ensureSpaceLimit(info, element);
			break;
		case IPHPElement.NAMESPACE_FRAGMENT:
			this.pkgCache.put(element, info);
			this.openableCache.ensureSpaceLimit(info, element);
			break;
		case IPHPElement.PROGRAM_UNIT:
		case IPHPElement.SCRIPT_FILE:
			this.openableCache.put(element, info);
			break;
		default:
			this.childrenCache.put(element, info);
		}
	}

	/**
	 * Removes the info of the element from the cache.
	 */
	protected void removeInfo(PHPElement element) {
		switch (element.getElementType()) {
		case IPHPElement.PHP_MODEL:
			this.modelInfo = null;
			break;
		case IPHPElement.PHP_PROJECT:
			this.projectCache.remove(element);
			this.rootCache.resetSpaceLimit((int) (DEFAULT_ROOT_SIZE * getMemoryRatio()), element);
			break;
		case IPHPElement.NAMESPACE_FRAGMENT_ROOT:
			this.rootCache.remove(element);
			this.pkgCache.resetSpaceLimit((int) (DEFAULT_PKG_SIZE * getMemoryRatio()), element);
			break;
		case IPHPElement.NAMESPACE_FRAGMENT:
			this.pkgCache.remove(element);
			this.openableCache.resetSpaceLimit((int) (DEFAULT_OPENABLE_SIZE * getMemoryRatio() * getOpenableRatio()),
					element);
			break;
		case IPHPElement.PROGRAM_UNIT:
		case IPHPElement.SCRIPT_FILE:
			this.openableCache.remove(element);
			break;
		default:
			this.childrenCache.remove(element);
		}
	}

	protected void resetJarTypeCache() {
		this.jarTypeCache = new LRUCache((int) (DEFAULT_OPENABLE_SIZE * getMemoryRatio()));
	}

	@Override
	public String toString() {
		return toStringFillingRation(""); //$NON-NLS-1$
	}

	public String toStringFillingRation(String prefix) {
		StringBuffer buffer = new StringBuffer();
		buffer.append(prefix);
		buffer.append("Project cache: "); //$NON-NLS-1$
		buffer.append(this.projectCache.size());
		buffer.append(" projects\n"); //$NON-NLS-1$
		buffer.append(prefix);
		buffer.append(this.rootCache.toStringFillingRation("Root cache")); //$NON-NLS-1$
		buffer.append('\n');
		buffer.append(prefix);
		buffer.append(this.pkgCache.toStringFillingRation("Package cache")); //$NON-NLS-1$
		buffer.append('\n');
		buffer.append(prefix);
		buffer.append(this.openableCache.toStringFillingRation("Openable cache")); //$NON-NLS-1$
		buffer.append('\n');
		buffer.append(prefix);
		buffer.append(this.jarTypeCache.toStringFillingRation("Jar type cache")); //$NON-NLS-1$
		buffer.append('\n');
		return buffer.toString();
	}
}
